package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @author vvv
 * @date 2023-10-30 15 44
 * discription
 */
@Component
public class CacheClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    // 各种缓存逻辑 其中锁key为 lockKeyPrefix + id, 缓存key为 cacheKey + id

    /**
     *  使用读写锁保证，缓存与数据库的读写，缓存与数据库的更新的强一致性
     *  需要配合 updateDBWithReadWriteLock一起使用
     * @param id
     * @param returnType
     * @param lockKey
     * @param cacheKey
     * @param dbFallback
     * @param timeout
     * @param unit
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithReadWriteLock(ID id, Class<R> returnType, String lockKey,
                                            String cacheKey, Function<ID, R> dbFallback,
                                            Long timeout, TimeUnit unit) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
        readWriteLock.readLock().lock();
        try {
            // 1. 先去redis中查缓存
            String json = stringRedisTemplate.opsForValue().get(cacheKey);
            // 2. 如果redis中有数据直接返回
            if (StringUtils.isNotBlank(json)) {
                return JSONUtil.toBean(json, returnType);
            }
            // 3. 如果数据是空值，缓存穿透
            if ("".equals(json)) {
                throw new RuntimeException("信息不存在");
            }
        } finally {
            readWriteLock.readLock().unlock();
        }

        // 读数据库，写缓存操作
        readWriteLock.writeLock().lock();
        try {
            // 5.3 去查mysql
            R r = dbFallback.apply(id);
            // 缓存穿透
            this.setWithTTL(cacheKey, r, timeout, unit);
        } finally {
            readWriteLock.writeLock().unlock();
        }
        return null;
    }

    /**
     * 互斥锁解决方案
     * @param id  id
     * @param returnType 返回类型
     * @param lockKey 锁的key
     * @param cacheKey 缓存key
     * @param dbFallback 查询数据库操作
     * @param timeout 过期时间
     * @param unit 时间单位
     * @return
     * @param <R> 返回类型
     * @param <ID> id 的类型
     * @throws InterruptedException
     */
    public <R, ID> R queryWithSync(ID id, Class<R> returnType, String lockKey,
                                   String cacheKey, Function<ID, R> dbFallback,
                                   long timeout, TimeUnit unit) {
        // 1. 先去redis中查缓存
        String json = stringRedisTemplate.opsForValue().get(cacheKey);
        // 2. 如果redis中有数据直接返回
        if (StringUtils.isNotBlank(json)) {
            return JSONUtil.toBean(json, returnType);
        }
        // 3. 如果数据是空值，缓存穿透
        if ("".equals(json)) {
            throw new RuntimeException("信息不存在");
        }
        // 4. redis中没有数据 json == null
        // 5. 尝试获取锁
        ILock myLock = new MyLock(lockKey, stringRedisTemplate);
        boolean isLock = myLock.tryLock();
        // 5.1 失败，则休眠自旋
        if (!isLock) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return this.queryWithSync(id, returnType, lockKey
                    , cacheKey, dbFallback, timeout, unit);
        }
        // 5.2 成功则根据id查询数据库构建缓存
        R r = null;
        try {
            /**
             *  这里需要做double check吗？
             *  不需要，因为这里获取不到锁的线程都自旋重试去了，并不会阻塞到lock哪里
             *  也就是说只有一个线程能到达这里
             */
            // 5.3 去查mysql
            r = dbFallback.apply(id);
            // 缓存穿透
            this.setWithTTL(cacheKey, r, timeout, unit);
        } finally {
            // 6. 释放锁
            myLock.unlock();
        }
        // 7. 返回
        return r;
    }

    /**
     * 互斥锁解决方案
     * @param id  id
     * @param lockKey 锁的key
     * @param cacheKey 缓存key
     * @param dbFallback 查询数据库操作
     * @param timeout 过期时间
     * @param unit 时间单位
     * @return
     * @param <R> 返回类型
     * @param <ID> id 的类型
     * @throws InterruptedException
     */
    public <R, ID> List<R> queryListWithSync(ID id, String lockKey,
                                             String cacheKey, Function<ID, List<R>> dbFallback,
                                             long timeout, TimeUnit unit) {
        ObjectMapper objectMapper = new ObjectMapper();
        // 1. 先去redis中查缓存
        String json = stringRedisTemplate.opsForValue().get(cacheKey);
        // 2. 如果redis中有数据直接返回
        if (StringUtils.isNotBlank(json)) {
            try {
                return objectMapper.readValue(json, new TypeReference<List<R>>() {
                });
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        // 3. 如果数据是空值，缓存穿透
        if ("".equals(json)) {
            throw new RuntimeException("信息不存在");
        }
        // 4. redis中没有数据 json == null
        // 5. 尝试获取锁
        ILock myLock = new MyLock(lockKey, stringRedisTemplate);
        boolean isLock = myLock.tryLock();
        // 5.1 失败，则休眠自旋
        if (!isLock) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return this.queryListWithSync(id, lockKey
                    , cacheKey, dbFallback, timeout, unit);
        }
        // 5.2 成功则根据id查询数据库构建缓存
        List<R> r ;
        try {
            /**
             *  这里需要做double check吗？
             *  不需要，因为这里获取不到锁的线程都自旋重试去了，并不会阻塞到lock哪里
             *  也就是说只有一个线程能到达这里
             */
            // 5.3 去查mysql
            r = dbFallback.apply(id);
            // 缓存穿透
            this.setWithTTL(cacheKey, r, timeout, unit);
        } finally {
            // 6. 释放锁
            myLock.unlock();
        }
        // 7. 返回
        return r;
    }



    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * 逻辑过期解决方案
     * @param id  id
     * @param returnType 返回类型
     * @param lockKey 锁的key
     * @param cacheKey 缓存key
     * @param dbFallback 查询数据库操作
     * @param timeout 过期时间
     * @param unit 时间单位
     * @return
     * @param <R> 返回类型
     * @param <ID> id 的类型
     * @throws InterruptedException
     */
    public <R, ID> R queryWithLogicalExpire(
            ID id, Class<R> returnType, String lockKey,
            String cacheKey, Function<ID, R> dbFallback,
            Long timeout, TimeUnit unit) {
        // 1. 先去redis中查缓存
        String json = stringRedisTemplate.opsForValue().get(cacheKey);
        // 2. 如果redis中没有数据直接返回
        if (StringUtils.isBlank(json)) {
            throw new RuntimeException("数据不存在");
        }
        // 3. 有数据
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        // 过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        // 数据本身
        R r = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), returnType);
        // 4. 判断是否过期
        if (LocalDateTime.now().isBefore(expireTime)) {
            // 4.1 没过期直接返回
            return r;
        }
        // 5. 过期,尝试获取锁
        ILock myLock = new MyLock(lockKey, stringRedisTemplate);
        boolean isLock = myLock.tryLock();
        // 5.1 没有拿到锁，直接返回
        if (!isLock) {
            return r;
        }
        // 5.2 获取锁成功
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 5.3 去查数据库
                R r1 = dbFallback.apply(id);
                // 5.4 写入缓存
                this.setWithLogicalExpire(cacheKey, r1, timeout, unit);
            } finally {
                myLock.unlock();
            }
        });
        return r;
    }

    /**
     * 把对象序列化成json写入redis + TTL
     * @param key redis key
     * @param obj
     * @param timeout
     * @param unit
     */
    public void setWithTTL(String key, Object obj, long timeout, TimeUnit unit) {
        // 缓存穿透
        if (obj == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            throw new RuntimeException("信息不存在");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj));
        if (timeout != RedisConstants.LOCK_NO_TTL) {
            stringRedisTemplate.expire(key, timeout, unit);
        }
    }

    /**
     * 把对象序列化成json写入redis + 逻辑过期
     * @param key redis key
     * @param obj
     * @param timeout
     * @param unit
     */
    public void setWithLogicalExpire(String key, Object obj, Long timeout, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(obj);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    // 缓存更新策略
    /**
     * 使用读写锁保证缓存与数据库的强一致
     * @param t 更新数据库的数据
     * @param dbOperation 数据库操作
     * @param cacheKey 缓存的key
     * @param <T>
     */
    @Transactional
    public <T> boolean updateDBWithReadWriteLock(T t, Function<T, Boolean> dbOperation, String cacheKey, String lockKey) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
        readWriteLock.writeLock().lock();
        Boolean isSuccess;
        try {
            // 这里顺序可以调换，因为这里是串行的
            // 1. 先更新数据库再删缓存
            isSuccess = dbOperation.apply(t);
            // 2. 删缓存
            stringRedisTemplate.delete(cacheKey);
        } finally {
            readWriteLock.writeLock().unlock();
        }
        return BooleanUtil.isTrue(isSuccess);
    }
    /**
     * 更新数据库再删缓存
     * @param t 更新数据库的数据
     * @param dbOperation 数据库操作
     * @param cacheKey 缓存的key
     * @param <T>
     */
    @Transactional
    public <T> boolean updateDBWithNoLock(T t, Function<T, Boolean> dbOperation, String cacheKey) {
        // 1. 先更新数据库再删缓存
        Boolean isSuccess = dbOperation.apply(t);
        // 2. 删缓存
        stringRedisTemplate.delete(cacheKey);
        return BooleanUtil.isTrue(isSuccess);
    }

    private static final ExecutorService DELAY_DOUBLE_DELETE = Executors.newSingleThreadExecutor();

    /**
     * 延时双删
     * @param t 更新数据库的数据
     * @param dbOperation 数据库操作
     * @param cacheKey 缓存的key
     * @param <T>
     */
    public <T> boolean delayDoubleDelete(T t, Function<T, Boolean> dbOperation, String cacheKey) {
        // 1. 先删除缓存
        stringRedisTemplate.delete(cacheKey);
        // 2. 更新数据库
        Boolean isSuccess = dbOperation.apply(t);
        // 3. 延时n秒后再删除缓存（异步操作）
        DELAY_DOUBLE_DELETE.submit(() -> {
            Boolean isDelete = false;
            do {
                try {
                    Thread.sleep(RedisConstants.DELETE_DELAY);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                isDelete = stringRedisTemplate.delete(cacheKey);
            } while(isDelete);
        });
        return BooleanUtil.isTrue(isSuccess);
    }
}
